Java 容器源码分析之 TreeMap
TreeMap 是一种基于红黑树实现的 Key-Value 结构。在使用集合视图在 HashMap 中迭代时,是不能保证迭代顺序的; LinkedHashMap 使用了双向链表,保证按照插入顺序或者访问顺序进行迭代。但是有些时候,我们可能需要按照键的大小进行按序迭代,或者在使用哈希表的同时希望按键值进行排序,这个时候 TreeMap 就有其用武之地了。 TreeMap 支持按键值进行升序访问,或者由传入的比较器(Comparator)来控制。
下面基于 JDK 8 的源码对 TreeMap 进行一个简单的分析。
1
|
public class TreeMap<K,V>
|
同 HashMap 一样, TreeMap 也继承了 AbstractMap,并实现了 Cloneable, Serializable 接口。不同的是, TreeMap 还实现 NavigableMap 接口。
NavigableMap 接口和 SortedMap
SortedMap 是一个扩展自 Map 的一个接口,对该接口的实现要保证所有的 Key 是完全有序的。
这个顺序一般是指 Key 的自然序(实现 Comparable 接口)或在创建 SortedMap 时指定一个比较器(Comparator)。当我们使用集合的视角(Collection View,由 entrySet、keySet 与 values 方法提供)来迭代时,就可以按序访问其中的元素。
插入 SortedMap 中的所有 Key 的类都必须实现 Comparable 接口(或者可以作为指定的 Comparator 的参数)。在比较两个 Key 时通过调用 k1.compareTo(k2)
(or comparator.compare(k1, k2)
),因而所有的 Key 都必须能够相互比较,否则会抛出 ClassCastException
的异常。
SortedMap 中 Key 的顺序必须和 equals 保持一致(consistent with equals),
即 k1.compareTo(k2) == 0
(or comparator.compare(k1, k2)
) 和 k1.equals(k2)
要有相同的布尔值。(Comparable 接口的实现不强制要求这一点,但通常都会遵守。)这是因为 Map 接口的定义中,比较 Key 是通过 equals 方法,而在 SortedMap 中比较 Key 则是通过 compareTo (or compare) 方法。如果不一致的,就破坏了 Map 接口的约定。
通过 SortedMap 可以获取其中的一段数据,如 subMap(K fromKey, K toKey)
, headMap(K toKey)
, tailMap(K fromKey)
等,所有的区间操作都是左闭右开的。也可以通过 firstKey()
和 lastKey()
来获取第一个和最后一个键。
NavigableMap 是 JDK 1.6 之后新增的接口,扩展了 SortedMap 接口,提供了一些导航方法(navigation methods)来返回最接近搜索目标的匹配结果。
lowerEntry(K key)
(orlowerKey(K key)
),小于给定 Key 的 Entry (or Key)floorEntry(K key)
(orfloorKey(K key)
),小于等于给定 Key 的 Entry (or Key)higherEntry(K key)
(orhigherKey(K key)
),大于给定 Key 的 Entry (or Key)ceilingEntry(K key)
(orceilingKey(K key)
),大于等于给定 Key 的 Entry (or Key)
这些方法都有重载的版本,来控制是否包含端点。subMap(K fromKey, K toKey)
, headMap(K toKey)
, tailMap(K fromKey)
等方法也是如此。
NavigableMap 可以按照 Key 的升序或降序进行访问和遍历。 descendingMap()
和 descendingKeySet()
则会获取和原来的顺序相反的集合,集合中的元素则是同样的引用,在该视图上的修改会影响到原始的数据。
底层结构
TreeMap 是基于红黑树来实现的,排序时按照键的自然序(要求实现 Comparable 接口)或者提供一个 Comparator 用于排序。
1
|
//比较器,没有指定的话默认使用Key的自然序
|
TreeMap 同样不是线程安全的,基于结构化修改的次数来实现 fail-fast 机制。因而要在多线程环境下使用时,可能需要手动进行同步,或者使用 Collections.synchronizedSortedMap
进行包装。
TreeMap 中的红黑树使用的是「算法导论」中的实现,除了左右链接、红黑标识以外,还有一个指向父节点的连接。红黑树的具体插入及删除细节这里不作过多的解释,更深入的细节可以参考「算法导论」一书,不过建议先看一下 Sedgewick 的讲解。
1
|
//Entry (红黑树节点的定义)
|
添加及更新操作
为了维持有序,添加及更新的代价较高,复杂度为 O(log(n)) 。插入节点后需要修复红黑树,使其恢复平衡状态,该操作在此不作介绍。
1
|
public V put(K key, V value) {
|
删除
从红黑树中删除一个节点比插入更为复杂,这里不作展开。
1
|
public V remove(Object key) {
|
1
|
private void deleteEntry(Entry<K,V> p) {
|
查找
红黑树也是排序二叉树,按照排序二叉树的查找方法进行查找。复杂度为 O(log(n)) 。
1
|
public V get(Object key) {
|
判断是否包含 key 或 value :
1
|
public boolean containsKey(Object key) {
|
导航方法
NaviableMap 接口支持一系列的导航方法,有 firstEntry()、 lastEntry()、 lowerEntry()、 higherEntry()、 floorEntry()、 ceilingEntry()、 pollFirstEntry() 、 pollLastEntry() 等,它们的实现原理都是类似的,区别在于如何在排序的二叉树中查找到对应的节点。
以 lowerEntry() 和 floorEntry() 为例:
1
|
//小于给定的Key
|
查找的过程可以和前驱节点的方法进行类比。 TreeMap 并没有直接暴露 getLowerEntry() 方法,而是使用 exportEntry(getLowerEntry(key))
进行了一次包装。看似“多此一举”,实际上是为了防止对节点进行修改。SimpleImmutableEntry 类可以看作不可修改的 Key-Value 对,因为成员变量 key 和 value 都是 final 的。
即通过暴露出来的接口 firstEntry()、 lastEntry()、 lowerEntry()、 higherEntry()、 floorEntry()、 ceilingEntry() 是不可以修改获取的节点的,否则会抛出异常。
1
|
/**
|
pollFirstEntry() 、 pollLastEntry() 获取第一个和最后一个节点,并将它们从红黑树中删除。
1
|
public Map.Entry<K,V> pollFirstEntry() {
|
遍历
可以按照键的顺序遍历对 TreeSet 进行遍历,因为底层使用了红黑树来保证有序性,迭代器的实现就是按序访问排序二叉树中的节点。
先看一些内部抽象类 PrivateEntryIterator
,它是 TreeMap 中所有迭代器的基础:
1
|
abstract class PrivateEntryIterator<T> implements Iterator<T> {
|
因为红黑树自身就是有序的,迭代是只要从第一个节点不断获取后继节点即可。当然,逆序时则是从最后一个节点不断获取前驱节点。通过迭代器访问时基于 modCount 实现对并发修改的检查。
在排序二叉树中获取前驱和后继节点的方法如下:
1
|
//后继节点
|
其它方法
TreeMap 中还实现了一些其它的方法,如区间操作: headMap(), tailMap(), subMap() ; 获取逆序的 map: descendingMap()
, descendingKeySet()
。只要了解了前面介绍的各种操作的原理,再来看这些方法的实现应该也不难理解。由于篇幅太长,这里就不再介绍了。
小结
TreeMap 是基于红黑树实现的一种 Key-Value 结构,最大的特点在于可以按照 Key 的顺序进行访问,要求 Key 实现 Comparable 接口或传入 Comparator 作为比较器。因为基于红黑树实现,TreeMap 内部在实现插入和删除操作时代价较高。
TreeMap 实现了 NavigableMap 接口,可以支持一系列导航方法,有 firstEntry()、 lastEntry()、 lowerEntry()、 higherEntry()、 floorEntry()、 ceilingEntry()、 pollFirstEntry() 、 pollLastEntry() ;还可以支持区间操作获取 map 的一部分,如 subMap(), headMap(), tailMap(K fromKey) 。除此以外, TreeMap 还支持通过 descendingMap() 获取和原来顺序相反的 map。
如果 TreeMap 没有使用自定义的 Comparator,则是不支持键为 null 的,因为调用 compareTo() 可能会发生异常;如果自定义的比较器可以接受 null 作为参数,那么是可以支持将 null 作为键的。
TreeMap 不是线程安全的,多线程情况下要手动进行同步或使用 SortedMap m = Collections.synchronizedSortedMap(new TreeMap(...));
。